Skip to content

#219: dispose the Thread event from free_obj as well - #220

Merged
EdmondDantes merged 3 commits into
mainfrom
219-thread-fatal-notify-handle-leak
Aug 11, 2026
Merged

#219: dispose the Thread event from free_obj as well#220
EdmondDantes merged 3 commits into
mainfrom
219-thread-fatal-notify-handle-leak

Conversation

@EdmondDantes

Copy link
Copy Markdown
Contributor

Closes #219.

php_error_cb() marks every live object as destructed before it bails out, so dtor_obj runs for none of them and teardown calls only free_obj. thread_object_dtor() was the sole caller of event->dispose(), and that dispose is the sole uv_close() of the cross-thread notify handle. After any fatal error the handle stayed open: uv_loop_close() returned EBUSY, and the loop's internals plus the persistent zend_async_thread_context_t leaked. A debug build reported one leftover libuv handle line per live thread.

free_obj now disposes the event too, and releases the finally-handler array. Dispatching those handlers stays in the dtor, since user code must not run on the fatal path.

Timers, sockets, signals and ThreadPool were checked on the same fatal path and release their handles already; only Thread was affected.

Test

tests/thread/081-thread_fatal_disposes_notify_handle.phpt runs four threads, keeps them reachable and exhausts the memory limit. A debug build names each surviving handle on stderr, which run-tests folds into the compared output, so the leak shows up as extra lines behind the fatal error — hence no trailing %A in --EXPECTF--.

Verified by reverting the fix: the test then fails on exactly the four leftover libuv handle lines. Full local suite with the fix: 1228 tests, 0 failures.

php_error_cb() marks every live object as destructed before it bails
out, so dtor_obj runs for none of them and teardown calls only
free_obj. thread_object_dtor() was the sole caller of event->dispose(),
and that dispose is the sole uv_close() of the cross-thread notify
handle. After any fatal error the handle stayed open: uv_loop_close()
returned EBUSY, and the loop's internals plus the persistent thread
context leaked.

free_obj now disposes the event too. Dispatching finally handlers
stays in the dtor, since user code must not run on the fatal path.
@codecov

codecov Bot commented Aug 11, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@EdmondDantes EdmondDantes self-assigned this Aug 11, 2026
@EdmondDantes EdmondDantes added the bug Something isn't working label Aug 11, 2026
@EdmondDantes EdmondDantes added this to the TrueAsync 0.8.0 milestone Aug 11, 2026
@EdmondDantes
EdmondDantes merged commit 580ffcc into main Aug 11, 2026
8 checks passed
@EdmondDantes
EdmondDantes deleted the 219-thread-fatal-notify-handle-leak branch August 11, 2026 11:23
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

A fatal error leaks the libuv notify handle of every live Async\Thread

1 participant